﻿using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace SpiderMonkey
{
    public delegate void ErrorReporter(Context context, string message, ErrorReport report);
    public delegate bool NativeFunction(Context context, JSVal[] arguments, out JSVal ret);
    public sealed class Context : NativeObject
    {
        internal static Context CurrentContext;
        private static Native.JSErrorReporter nativeErrorReporter = ContextErrorReporter;
        private static Dictionary<IntPtr, Context> contexts = new Dictionary<IntPtr, Context>();
        private ErrorReporter reporter;

        /// <summary>
        /// Create a new JSContext.
        /// </summary>
        /// <param name="runtime">Parent runtime for the new context. JavaScript objects, functions, strings, and numbers may be shared among the contexts in a JSRuntime, but they cannot be shared across JSRuntimes.</param>
        /// <param name="stackChunkSize">The size, in bytes, of each "stack chunk". This is a memory management tuning parameter which most users should not adjust. 8192 is a good default value.</param>
        public Context(Runtime runtime, int stackChunkSize = 8192)
        {
            CurrentContext = this;
            if (runtime == null) throw new ArgumentNullException("runtime");

            IntPtr ptr = Native.JS_NewContext(runtime.Pointer, stackChunkSize);
            if (ptr == IntPtr.Zero) throw new SpiderException();
            Setup(ptr);

            lock(contexts) contexts[ptr] = this;

            Native.JS_SetErrorReporter(Pointer, Marshal.GetFunctionPointerForDelegate(nativeErrorReporter));
        }
        private Context(IntPtr ptr, bool delete)
            : base(ptr, delete)
        {
        }

        /// <summary>
        /// Enables and disables options on a JSContext, replacing all previously set options.
        /// </summary>
        /// <param name="options">The new set of options. This is the logical OR of zero or more flags described below.</param>
        public Option SetOptions(Option options)
        {
            return (Option)Native.JS_SetOptions(Pointer, (uint)options);
        }
        /// <summary>
        /// Configure a JSContext to use a specific version of the JavaScript language.
        /// </summary>
        /// <param name="version">Version of JavaScript to set context to.</param>
        public Version SetVersion(Version version)
        {
            return Native.JS_SetVersion(Pointer, version);
        }
        public ErrorReporter SetErrorReporter(ErrorReporter reporter)
        {
            ErrorReporter old = this.reporter;
            this.reporter = reporter;
            return old;
        }
        public void InitStandardClasses(JSObject global)
        {
            if (Native.JS_InitStandardClasses(Pointer, global.Pointer) == 0) throw new SpiderException();
        }
        public bool EvaluateScript(JSObject obj, string source, string fileName, int lineNumber, out JSVal val)
        {
            ulong nativeVal;
            int ret = Native.JS_EvaluateScript(Pointer, obj.Pointer, source, (uint)source.Length, fileName, lineNumber, out nativeVal);
            val = new JSVal(nativeVal);
            return ret != 0;
        }
        public Script CompileFile(JSObject obj, string file)
        {
            IntPtr ptr = Native.JS_CompileFile(Pointer, obj.Pointer, file);
            if (ptr == IntPtr.Zero) throw new SpiderException();
            return new Script(ptr);
        }
        public bool ExecuteScript(JSObject obj, Script script, out JSVal val)
        {
            ulong nativeVal;
            int ret = Native.JS_ExecuteScript(Pointer, obj.Pointer, script.Pointer, out nativeVal);
            val = new JSVal(nativeVal);
            return ret != 0;
        }

        protected override void Dispose(bool disposing)
        {
            Native.JS_DestroyContext(Pointer);
        }

        private static void ContextErrorReporter(IntPtr cx, string message, IntPtr report)
        {
            Context ctx = contexts[cx];
            if (ctx == null) throw new SpiderException(); //This should never happen
            if (ctx.reporter == null) return;

            ErrorReport clrReport = new ErrorReport(report);
            ctx.reporter(ctx, message, clrReport);
        }
        internal static Context Reference(IntPtr cx)
        {
            return new Context(cx, false);
        }
        public void DefineFunction(JSObject obj, string functionName, NativeFunction function, int argCount, int flags)
        {
            Native.JSNative native = delegate(IntPtr cx, int argc, IntPtr vp)
            {
                Context ctx = contexts[cx];
                JSVal[] vals = new JSVal[argc];
                for (int i = 0; i < argc; ++i)
                    vals[i] = new JSVal((ulong)Marshal.ReadInt64(vp, sizeof(long) * (2 + i)));
                JSVal ret;
                bool val = function(ctx, vals, out ret);
                Marshal.WriteInt64(vp, 0, (long)ret.Pointer);
                return val ? 1 : 0;
            };
            if(Native.JS_DefineFunction(Pointer, obj.Pointer, functionName, native, argCount, flags) == IntPtr.Zero) throw new SpiderException();
        }
        public void DefineFunction(JSObject obj, string functionName, Delegate function)
        {
            DefineFunction(obj, functionName, delegate(Context context, JSVal[] arguments, out JSVal ret)
            {
                object[] clrArgs = new object[arguments.Length];
                for (int i = 0; i < clrArgs.Length; ++i) clrArgs[i] = arguments[i].ToObject();
                try
                {
                    object clrRet = function.DynamicInvoke(clrArgs);
                    ret = function.Method.ReturnType == typeof(void) ? JSVal.Undefined : JSVal.Create(clrRet);
                    return true;
                }
                catch
                {
                    ret = JSVal.Undefined;
                    return false;
                }
            }, function.Method.GetParameters().Length, 0);
        }
    }
}
